/*
This file is part of MMM.

MMM is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

MMM is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with MMM.  If not, see <http://www.gnu.org/licenses/>.
*
* @package    MMM
* @author     Andre Meixner
* @copyright  2017 High Performance Humanoid Technologies (H2T), Karlsruhe, Germany
*
*/

#ifndef __MMM_RAPIDXMLREADER_H_
#define __MMM_RAPIDXMLREADER_H_

#include "rapidxml.hpp"
#include "../Exceptions.h"

#include <MMM/MMMCore.h>
#include <boost/shared_ptr.hpp>
#include <boost/shared_array.hpp>
#include <string>
#include <fstream>
#include <streambuf>

namespace MMM
{
    class RapidXMLReader;
    typedef boost::shared_ptr<RapidXMLReader> RapidXMLReaderPtr;

    class RapidXMLReaderNode;
    typedef boost::shared_ptr<RapidXMLReaderNode> RapidXMLReaderNodePtr;

    //! @brief Represents an xml node in the RapidXMLReader
    class RapidXMLReaderNode
    {
        friend class RapidXMLReader;

    private:
        rapidxml::xml_node<>* node;
        boost::shared_ptr<rapidxml::xml_document<> > doc;
        RapidXMLReaderNode(rapidxml::xml_node<>* node, boost::shared_ptr<rapidxml::xml_document<> > doc)
            : node(node), doc(doc)
        { }
        void check() const
        {
            if (!node)
            {
                throw Exception::XMLFormatException("RapidXmlWrapper NullPointerException");
            }
        }

    public:
        static RapidXMLReaderNodePtr NullNode()
        {
            RapidXMLReaderNodePtr wrapper(new RapidXMLReaderNode(NULL, boost::shared_ptr<rapidxml::xml_document<> >()));
            return wrapper;
        }

        /**
         * @brief get_node_ptr only for legacy code.
         * @return internal pointer
         */
        rapidxml::xml_node<>* get_node_ptr() const
        {
            return node;
        }

        /*! Returns the first child node (optional: with the given name).
            @throws XMLFormatException if the node does not exist. */
        RapidXMLReaderNodePtr first_node(const char* name = 0) const
        {
            check();

            rapidxml::xml_node<>* node = this->node->first_node(name, 0, false);

            if (!node)
            {
                throw Exception::XMLFormatException(std::string("Node '") + name + "' does not exist in node " + getPath());
            }

            RapidXMLReaderNodePtr wrapper(new RapidXMLReaderNode(node, doc));
            return wrapper;
        }

        /*! Returns the first child node with the given name and attribute. */
        RapidXMLReaderNodePtr first_node_with_attribute(const char* name, const std::string &attrName, const std::string &attrValue) const
        {
            check();
            std::vector<RapidXMLReaderNodePtr> n = nodes(name);
            for (std::vector<RapidXMLReaderNodePtr>::iterator it; n.begin(), it != n.end(); ++it) {
                if ((*it)->has_attribute(attrName.c_str()) && (*it)->attribute_value(attrName.c_str()) == attrValue) {
                    return (*it);
                }
            }
            return NullNode();
        }

        /*! Returns all child nodes (optional: just with the given name). */
        std::vector<RapidXMLReaderNodePtr> nodes(const char* name = 0) const
        {
            std::vector<RapidXMLReaderNodePtr> vec;
            nodes(name, vec);
            return vec;
        }

        void nodes(const char* name, std::vector<RapidXMLReaderNodePtr>& vec) const
        {
            for (RapidXMLReaderNodePtr n = first_node(name); n->is_valid(); n = n->next_sibling(name))
            {
                vec.push_back(n);
            }
        }

        /*! Returns the attribute value.
            @param attrName The name of the attribute.
            @throws XMLFormatException if the attribute does not exist. */
        std::string attribute_value(const char* attrName) const
        {
            check();
            rapidxml::xml_attribute<>* attrib = node->first_attribute(attrName, 0, false);

            if (!attrib)
            {
                throw Exception::XMLFormatException(std::string("Attribute '") + attrName + "' does not exist in node " + getPath());
            }

            return std::string(attrib->value());
        }

        /*! Checks if a node has a specific attribute.
            @param attrName The name of the attribute.*/
        bool has_attribute(const char* attrName) const
        {
            check();
            return node->first_attribute(attrName, 0, false) != 0;
        }

        /*! Checks if a node has a specific child node.
            @param attrName The name of the child node.*/
        bool has_node(const char* nodeName) const
        {
            check();
            return node->first_node(nodeName, 0, false) != 0;
        }

        /*! Returns the value of an attribute if the attribute exists, otherwise a default value.
            @param attrName The name of the attribute.
            @param defaultValue The default value.*/
        std::string attribute_value_or_default(const char* attrName, const std::string& defaultValue) const
        {
            check();
            rapidxml::xml_attribute<>* attrib = node->first_attribute(attrName, 0, false);

            if (!attrib)
            {
                return defaultValue;
            }

            return std::string(attrib->value());
        }

        bool attribute_as_bool(const char* name, const std::string& trueValue, const std::string& falseValue) const
        {
            check();
            rapidxml::xml_attribute<>* attrib = node->first_attribute(name, 0, false);

            if (!attrib)
            {
                throw Exception::XMLFormatException(std::string("Attribute '") + name + "' does not exist.");
            }

            std::string value = std::string(attrib->value());

            if (value == trueValue)
            {
                return true;
            }
            else if (value == falseValue)
            {
                return false;
            }
            else
            {
                throw Exception::XMLFormatException(std::string("Invalid value '") + value + "' for attribute '" + name + "'. Expecting '" + trueValue + "' or '" + falseValue + "'.");
            }
        }

        bool attribute_as_optional_bool(const char* name, const std::string& trueValue, const std::string& falseValue, bool defaultValue) const
        {
            check();
            rapidxml::xml_attribute<>* attrib = node->first_attribute(name, 0, false);

            if (!attrib)
            {
                return defaultValue;
            }

            std::string value = std::string(attrib->value());

            if (value == trueValue)
            {
                return true;
            }
            else if (value == falseValue)
            {
                return false;
            }
            else
            {
                throw Exception::XMLFormatException(std::string("Invalid value '") + value + "' for attribute '" + name + "'. Expecting '" + trueValue + "' or '" + falseValue + "'.");
            }
        }

        std::vector<std::pair<std::string, std::string> > get_all_attributes()
        {
            check();
            std::vector<std::pair<std::string, std::string> > attributes;

            rapidxml::xml_attribute<>* attrib = node->first_attribute(0, 0, false);

            while (attrib)
            {
                std::string name = std::string(attrib->name());
                std::string value = std::string(attrib->value());
                std::pair<std::string, std::string> attrPair(name, value);
                attributes.push_back(attrPair);
                attrib = attrib->next_attribute();
            }

            return attributes;
        }

        /*! Returns the content of an xml node.*/
        std::string value() const
        {
            check();
            return std::string(node->value());
        }

        /*! Returns the name of an xml node.*/
        std::string name() const
        {
            check();
            return std::string(node->name());
        }

        rapidxml::node_type type()
        {
            check();
            return node->type();
        }

        std::string first_node_value(const char* nodeName = 0) const
        {
            check();
            rapidxml::xml_node<>* childNode = node->first_node(nodeName, 0, false);

            if (!childNode)
            {
                throw Exception::XMLFormatException(std::string("Node '") + nodeName + "' does not exist in node " + getPath());
            }

            return std::string(childNode->value());
        }

        std::string first_node_value_or_default(const char* name, const std::string& defaultValue) const
        {
            check();
            rapidxml::xml_node<>* childNode = node->first_node(name, 0, false);

            if (!childNode)
            {
                return defaultValue;
            }

            return std::string(childNode->value());
        }
        RapidXMLReaderNodePtr next_sibling(const char* name = 0) const
        {
            check();
            RapidXMLReaderNodePtr wrapper(new RapidXMLReaderNode(node->next_sibling(name, 0, false), doc));
            return wrapper;
        }

        bool is_valid() const
        {
            return node != NULL;
        }

        std::string getPath() const
        {
            check();
            std::string result = name();
            rapidxml::xml_node<>* p = node->parent();

            while (p)
            {
                result = std::string(p->name()) + "/" + result;
                p = p->parent();
            }

            return result;
        }

    };

    //! @brief Helper class for reading information from an xml format via Rapid XML
    class RapidXMLReader
    {
    private:
        boost::shared_ptr<rapidxml::xml_document<> > doc;
        char* cstr;

        RapidXMLReader(const std::string& xml)
            : doc(new rapidxml::xml_document<>())
        {
            if (xml.empty()) throw Exception::XMLFormatException(std::string("Empty xml!"));
            cstr = new char[xml.size() + 1];        // Create char buffer to store string copy
            strcpy(cstr, xml.c_str());              // Copy string into char buffer
            try {
                doc->parse<0>(cstr);                    // Pass the non-const char* to parse()
            }
            catch (rapidxml::parse_error& e) {
                throw Exception::XMLFormatException(std::string("No valid xml format!"));
            }
        }

    public:
        ~RapidXMLReader()
        {
            delete[] cstr;                          // free buffer memory when all is finished
        }

    public:
        static std::string ReadFileContents(const std::string& path)
        {
            std::ifstream in(path.c_str());

            if (!in.is_open())
            {
                MMM_ERROR << "Could not open XML file:" << path << endl;
                return "";
            }

            std::stringstream buffer;
            buffer << in.rdbuf();
            std::string xmlString(buffer.str());
            in.close();

            return xmlString;
        }

        /*!
           Creates a RapidXMLReader.
            @param xml The xml as string.
            @return The RapidXMLReader for the xml string.
        */
        static RapidXMLReaderPtr FromXmlString(const std::string& xml)
        {
            RapidXMLReaderPtr wrapper(new RapidXMLReader(xml));
            return wrapper;
        }

        /*!
            Creates a RapidXMLReader.
            @param xml Path to an xml document.
            @return The RapidXMLReader for the xml document.
        */
        static RapidXMLReaderPtr FromFile(const std::string& path)
        {
            return FromXmlString(ReadFileContents(path));
        }

        RapidXMLReaderNodePtr getDocument()
        {
            RapidXMLReaderNodePtr wrapper(new RapidXMLReaderNode(doc.get(), doc));
            return wrapper;
        }

        /*!
            Returns the root node of the xml document.
            @param xml The name of the root node.
            @return The root node of the underlying xml.
        */
        RapidXMLReaderNodePtr getRoot(const char* name = 0)
        {
            return getDocument()->first_node(name);
        }

    };
}


#endif
